luci-app-wol: Enables persistent configuration of hosts to wake up
authormdevolde <[email protected]>
Fri, 12 Dec 2025 15:35:15 +0000 (16:35 +0100)
committerPaul Donald <[email protected]>
Sun, 14 Dec 2025 19:55:16 +0000 (20:55 +0100)
Signed-off-by: Martin Devolder <[email protected]>
applications/luci-app-wol/htdocs/luci-static/resources/view/wol.js
applications/luci-app-wol/root/etc/config/luci-wol [new file with mode: 0644]
applications/luci-app-wol/root/usr/share/rpcd/acl.d/luci-app-wol.json
applications/luci-app-wol/root/usr/share/rpcd/ucode/luci.wol

index f67e9d1ccf6ca53b87278e0b7b2841f99010989e..0a315d3c3f9016760e4b02982250c9daba1509aa 100644 (file)
@@ -1,8 +1,6 @@
 'use strict';
 'require view';
-'require dom';
 'require uci';
-'require fs';
 'require ui';
 'require rpc';
 'require form';
 const ETHERWAKE_BIN = '/usr/bin/etherwake';
 const WAKEONLAN_BIN = '/usr/bin/wakeonlan';
 
+const PACKAGES_URL = 'admin/system/package-manager';
+
 return view.extend({
-       formdata: { wol: {} },
+       outputText: '',
 
        callStat: rpc.declare({
                object: 'luci.wol',
                method: 'stat',
-               params: [ ],
-               expect: { }
+               params: [],
+               expect: {}
        }),
 
        callExec: rpc.declare({
                object: 'luci.wol',
                method: 'exec',
-               params: [ 'name', 'args' ],
-               expect: { }
+               params: ['name', 'args'],
+               expect: {}
        }),
 
        callHostHints: rpc.declare({
                object: 'luci-rpc',
                method: 'getHostHints',
-               expect: { '': {} }
+               expect: {
+                       '': {}
+               }
        }),
 
-       load: function() {
+       option_install_etherwake() {
+               window.open(L.url(PACKAGES_URL) +
+                       '?query=etherwake', '_blank', 'noopener');
+       },
+
+       option_install_wakeonlan() {
+               window.open(L.url(PACKAGES_URL) +
+                       '?query=wakeonlan', '_blank', 'noopener');
+       },
+
+       load() {
                return Promise.all([
                        L.resolveDefault(this.callStat()),
                        this.callHostHints(),
-                       uci.load('etherwake')
+                       uci.load('luci-wol')
                ]);
        },
 
        render([stat, hosts]) {
-                       var has_ewk = stat && stat.etherwake,
-                               has_wol = stat && stat.wakeonlan,
-                   m, s, o;
+               const has_ewk = stat && stat.etherwake,
+                       has_wol = stat && stat.wakeonlan;
+               let m, s, o;
 
-               this.formdata.has_ewk = has_ewk;
-               this.formdata.has_wol = has_wol;
+               // Check if at least one Wake on LAN utility is available, else show install buttons
+               if (!has_ewk && !has_wol) {
+                       m = new form.Map('luci-wol', _('Wake on LAN'),
+                               _('Wake on LAN is a mechanism to boot computers remotely in the local network.'));
 
-               m = new form.JSONMap(this.formdata, _('Wake on LAN'),
+                       s = m.section(form.NamedSection, 'packages', 'packages',
+                               _('Required Packages'),
+                               _('At least one Wake on LAN utility is needed. Please install one of the following packages (some extra permissions may be required):'));
+
+                       s.render = L.bind(function(view) {
+                               return form.NamedSection.prototype.render.apply(this, arguments)
+                                       .then(L.bind(function(node) {
+                                               node.appendChild(E('div', {
+                                                       'class': 'control-group'
+                                               }, [
+                                                       E('button', {
+                                                               'class': 'btn cbi-button-action',
+                                                               'click': ui.createHandlerFn(view, 'option_install_etherwake', this.map),
+                                                               'title': _('Install etherwake package')
+                                                       }, [_('Install etherwake')]),
+                                                       ' ',
+                                                       E('button', {
+                                                               'class': 'btn cbi-button-action',
+                                                               'click': ui.createHandlerFn(view, 'option_install_wakeonlan', this.map),
+                                                               'title': _('Install wakeonlan package')
+                                                       }, [_('Install wakeonlan')])
+                                               ]));
+                                               return node;
+                                       }, this));
+                       }, s, this);
+
+                       return m.render();
+               }
+
+               m = new form.Map('luci-wol', _('Wake on LAN'),
                        _('Wake on LAN is a mechanism to boot computers remotely in the local network.'));
 
-               s = m.section(form.NamedSection, 'wol');
+               // Default settings section (used executable)
+               s = m.section(form.NamedSection, 'defaults', 'wol', _('Default Settings'));
 
                if (has_ewk && has_wol) {
-                       o = s.option(form.ListValue, 'executable', _('WoL program'),
-                               _('Sometimes only one of the two tools works. If one fails, try the other one'));
-
+                       o = s.option(form.ListValue, 'executable', _('Default WoL program'),
+                               _('Choose the default Wake on LAN utility'));
                        o.value(ETHERWAKE_BIN, 'Etherwake');
                        o.value(WAKEONLAN_BIN, 'Wakeonlan');
-               }
-
-               if (has_ewk) {
-                       o = s.option(widgets.DeviceSelect, 'iface', _('Network interface to use'),
-                               _('Specifies the interface the WoL packet is sent on'));
-
-                       o.default = uci.get('etherwake', 'setup', 'interface');
-                       o.rmempty = false;
-                       o.noaliases = true;
-                       o.noinactive = true;
+                       o.default = ETHERWAKE_BIN;
+                       o.onchange = function(ev, section_id, value) {
+                               return m.save(null, true);
+                       };
+               } else {
+                       // If only one binary is available, show info message with install button for the other
+                       o = s.option(form.DummyValue, '_info');
+                       o.rawhtml = true;
+                       o.default = E('div', {}, [
+                               E('p', {}, [
+                                       _('Binary used') + ': ',
+                                       E('strong', {}, has_ewk ? 'Etherwake' : 'Wakeonlan')
+                               ]),
+                               E('p', {
+                                               'style': 'margin-top: 10px'
+                                       },
+                                       _('You can also install the alternative Wake on LAN utility (some extra permissions may be required):')),
+                               E('div', {
+                                       'class': 'control-group'
+                               }, [
+                                       E('button', {
+                                               'class': 'btn cbi-button-action',
+                                               'click': ui.createHandlerFn(this, has_ewk ? 'option_install_wakeonlan' : 'option_install_etherwake'),
+                                               'title': _('Install the alternative Wake on LAN package')
+                                       }, [_('Install %s').format(has_ewk ? 'wakeonlan' : 'etherwake')])
+                               ])
+                       ]);
 
-                       uci.sections('etherwake', 'target', function(section) {
-                               if (section.mac && section.name) {
-                                       // Create a host entry if it doesn't exist
-                                       if (!hosts[section.mac]) {
-                                               hosts[section.mac] = { name: section.name };
-                                       }
-                               }
-                       });
-
-                       if (has_wol)
-                               o.depends('executable', ETHERWAKE_BIN);
                }
 
-               o = s.option(form.Value, 'mac', _('Host to wake up'),
-                       _('Choose the host to wake up or enter a custom MAC address to use'));
+               // Targets section with GridSection
+               s = m.section(form.GridSection, 'target', _('Wake on LAN Targets'), _('Configure hosts that can be woken up. Click the Wake button to send a magic packet.') + '<br>' + _('Note: wakeonlan binary does not support interface, broadcast, and password options (etherwake only).') + ' ' + _('These options will be ignored if wakeonlan is used.'));
 
+               s.addremove = true;
+               s.anonymous = true;
+               s.sortable = true;
+               s.nodescriptions = true;
+
+               // Name column
+               o = s.option(form.Value, 'name', _('Name'), _('Mandatory'));
                o.rmempty = false;
+               o.datatype = 'string';
 
-               L.sortedKeys(hosts).forEach(function(mac) {
-                       o.value(mac, E([], [ mac, ' (', E('strong', [
-                               hosts[mac].name ||
+               // MAC address column
+               o = s.option(form.Value, 'mac', _('MAC Address'), _('Mandatory'));
+               o.rmempty = false;
+               o.datatype = 'macaddr';
+               L.sortedKeys(hosts).forEach(function(mac) { // Add host hints, need 'getHostHints' acl (luci-rpc)
+                       const hint = hosts[mac].name ||
                                L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0] ||
-                               L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6)[0] ||
-                               '?'
-                       ]), ')' ]));
+                               L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6)[0];
+                       o.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
                });
 
+               // Interface column (only for etherwake)
                if (has_ewk) {
-                       o = s.option(form.Flag, 'broadcast', _('Send to broadcast address'));
+                       o = s.option(widgets.DeviceSelect, 'iface', _('Interface'), _('Etherwake only')); // Network device selector widget, needs 'getNetworkDevices' acl (luci-rpc)
+                       o.noaliases = true;
+                       o.noinactive = true;
+               }
 
-                       if (has_wol)
-                               o.depends('executable', ETHERWAKE_BIN);
+               // Broadcast flag (only for etherwake)
+               if (has_ewk) {
+                       o = s.option(form.Flag, 'broadcast', _('Broadcast'), _('Etherwake only'));
+                       o.default = o.disabled;
+               }
+
+               // Password field (only for etherwake)
+               if (has_ewk) {
+                       o = s.option(form.Value, 'password', _('Password'), _('Etherwake only'));
+                       o.datatype = 'string';
+                       o.placeholder = '00:22:44:66:88:aa or 192.168.1.1';
+                       o.datatype = 'or(macaddr,ip4addr("nomask"))'; // Accept MAC or IPv4 address format
                }
 
+               // When editing, set modal title to include target name
+               s.modaltitle = L.bind(function(section_id) {
+                       var name = uci.get('luci-wol', section_id, 'name');
+                       return _('Edit target') + (name ? ': ' + name : '');
+               }, this);
+
+               // Keep reference to GridSection for button handlers
+               const gridSection = s;
+
+               // Take default row actions and add "Wake" button
+               s.renderRowActions = L.bind(function(section_id) {
+                       const defaultButtons = form.GridSection.prototype.renderRowActions.call(gridSection, section_id, _('Edit'));
+
+                       const wakeButton = E('button', {
+                               'class': 'cbi-button cbi-button-action',
+                               'click': ui.createHandlerFn(this, function() {
+                                       return this.handleWakeup(section_id, has_ewk, has_wol);
+                               })
+                       }, _('Wake'));
+
+                       const buttonContainer = defaultButtons.querySelector('div');
+                       if (buttonContainer) {
+                               buttonContainer.insertBefore(wakeButton, buttonContainer.firstChild);
+                       }
+
+                       return defaultButtons;
+               }, this);
+
+               // Output section, for wake results
+               s = m.section(form.NamedSection, 'output', 'wol', _('Output'));
+               s.anonymous = true;
+               s.render = L.bind(function() {
+                       return E('div', {
+                               'class': 'cbi-section'
+                       }, [
+                               E('h3', {}, _('Output')),
+                               E('textarea', {
+                                       'readonly': true,
+                                       'rows': 10,
+                                       'style': 'width: 100%; font-family: monospace;',
+                                       'id': 'wol-output-text'
+                               }, this.outputText)
+                       ]);
+               }, this);
+
                return m.render();
        },
 
-       handleWakeup: function(ev) {
-               var map = document.querySelector('#maincontent .cbi-map'),
-                   data = this.formdata,
-                   self = this;
+       handleWakeup(section_id, has_ewk, has_wol) {
+               const self = this;
+               const name = uci.get('luci-wol', section_id, 'name');
+               const mac = uci.get('luci-wol', section_id, 'mac');
 
-               return dom.callClassMethod(map, 'save').then(function() {
-                       if (!data.wol.mac)
-                               return alert(_('No target host specified!'));
+               // Determine which binary to use and verify availability
+               const defaultBin = uci.get('luci-wol', 'defaults', 'executable');
+               let bin = defaultBin || (has_ewk ? ETHERWAKE_BIN : WAKEONLAN_BIN);
 
-                       var bin = data.wol.executable || (data.has_ewk ? ETHERWAKE_BIN : WAKEONLAN_BIN),
-                           args = [];
+               if (bin == ETHERWAKE_BIN && !has_ewk)
+                       bin = WAKEONLAN_BIN;
+               else if (bin == WAKEONLAN_BIN && !has_wol)
+                       bin = ETHERWAKE_BIN;
 
-                       if (bin == ETHERWAKE_BIN) {
-                               args.push('-D', '-i', data.wol.iface);
+               // Build argument list based on selected binary
+               const args = [];
 
-                               if (data.wol.broadcast == '1')
-                                       args.push('-b');
+               if (bin == ETHERWAKE_BIN) {
+                       args.push('-D');
+                       const iface = uci.get('luci-wol', section_id, 'iface');
+                       if (iface)
+                               args.push('-i', iface);
 
-                               args.push(data.wol.mac);
-                       }
-                       else {
-                               args.push(data.wol.mac);
-                       }
+                       const broadcast = uci.get('luci-wol', section_id, 'broadcast');
+                       if (broadcast == '1')
+                               args.push('-b');
 
-                       ui.showModal(_('Waking host'), [
-                               E('p', { 'class': 'spinning' }, [ _('Starting WoL utility…') ])
-                       ]);
-                       
-                       return self.callExec(bin, args).then(function(res) {
-                               ui.showModal(_('Waking host'), [
-                                       res.stdout ? E('p', [ res.stdout ]) : '',
-                                       res.stderr ? E('pre', [ res.stderr ]) : '',
-                                       E('div', { 'class': 'right' }, [
-                                               E('button', {
-                                                       'class': 'cbi-button cbi-button-primary',
-                                                       'click': ui.hideModal
-                                               }, [ _('Dismiss') ])
-                                       ])
-                               ]);
-                       }).catch(function(err) {
-                               ui.hideModal();
-                               ui.addNotification(null, [
-                                       E('p', [ _('Waking host failed: '), err ])
-                               ]);
-                       });
+                       const password = uci.get('luci-wol', section_id, 'password');
+                       if (password)
+                               args.push('-p', password);
+
+                       args.push(mac);
+               } else {
+                       args.push(mac);
+               }
+
+               // Execute the wake command and handle output
+               this.appendOutput(`Sending wakeup to ${name} (${mac})...\n`);
+
+               return this.callExec(bin, args).then(function(res) {
+                       if (res.stdout)
+                               self.appendOutput(res.stdout + '\n');
+                       if (res.stderr)
+                               self.appendOutput('Error: ' + res.stderr + '\n');
+                       if (!res.stdout && !res.stderr)
+                               self.appendOutput('Command completed with code ' + (res.code || 0) + '\n');
+                       self.appendOutput('\n');
+               }).catch(function(err) {
+                       self.appendOutput('Error: ' + err + '\n\n');
                });
        },
 
-       addFooter: function() {
-               return E('div', { 'class': 'cbi-page-actions' }, [
-                       E('button', {
-                               'class': 'cbi-button cbi-button-save',
-                               'click': L.ui.createHandlerFn(this, 'handleWakeup')
-                       }, [ _('Wake up host') ])
-               ]);
+       appendOutput(text) {
+               // Append text to the output textarea and scroll to bottom
+               this.outputText += text;
+               const textarea = document.getElementById('wol-output-text');
+               if (textarea) {
+                       textarea.value = this.outputText;
+                       textarea.scrollTop = textarea.scrollHeight;
+               }
        }
 });
diff --git a/applications/luci-app-wol/root/etc/config/luci-wol b/applications/luci-app-wol/root/etc/config/luci-wol
new file mode 100644 (file)
index 0000000..2c94f01
--- /dev/null
@@ -0,0 +1 @@
+config wol 'defaults'
index b2bece606c47b7039b0b373192e3379f379e75bc..84895d6051a73057405e2a7acfd3a3b06c0cdd0d 100644 (file)
@@ -6,12 +6,14 @@
                                "luci.wol": [ "stat" ],
                                "luci-rpc": [ "getHostHints", "getNetworkDevices" ]
                        },
-                       "uci": [ "etherwake" ]
+                       "uci": [ "luci-wol" ]
                },
                "write": {
                        "ubus": {
-                               "luci.wol": [ "exec" ]
-                       }
+                               "luci.wol": [ "exec" ],
+                               "uci": [ "add", "set", "delete", "order" ]
+                       },
+                       "uci": [ "luci-wol" ]
                }
        }
 }
index 9d1e26f1c28cb016e7074720773963cf4e9750e8..7e410a4ad094995937f5b9ef16f059120ead62bf 100644 (file)
@@ -43,7 +43,7 @@ const methods = {
 
                                result.stdout = fd.read('all');
                                result.stderr = '';
-                               result.code = 0;
+                               result.code = fd.close();
                        } else {
                                result.stdout = '';
                                result.stderr = 'disallowed';